"""Scheduled task scheduler - timed downloads and automation."""

from collections.abc import Callable
from datetime import datetime
from enum import Enum

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.interval import IntervalTrigger
from loguru import logger


class ScheduleType(str, Enum):
    """Type of schedule for tasks.

    Attributes:
        ONCE (str): Single execution.
        DAILY (str): Daily execution.
        WEEKLY (str): Weekly execution.
        INTERVAL (str): Interval-based execution.
        CRON (str): Cron expression-based execution.
    """


class TaskScheduler:
    """Manages scheduled tasks using `apscheduler`.

    This class provides a convenient interface for scheduling various types of tasks,
    including one-time, daily, weekly, interval-based, and cron-based jobs.
    It uses `AsyncIOScheduler` internally to handle asynchronous task execution.
    """

    def __init__(self) -> None:
        """Initializes the TaskScheduler.

        Sets up the underlying `AsyncIOScheduler` and an internal dictionary
        to keep track of scheduled job IDs and their names.
        """
        self.scheduler = AsyncIOScheduler()
        self._jobs: dict[str, str] = {}

    def start(self) -> None:
        """Starts the scheduler.

        If the scheduler is not already running, it will be started and a log
        message will be recorded to indicate its activation.
        """
        if not self.scheduler.running:
            self.scheduler.start()
            logger.info("Task scheduler started")

    def shutdown(self, wait: bool = True) -> None:
        """Shuts down the scheduler.

        Args:
            wait (bool): If True, the scheduler will wait for all currently executing
                jobs to complete before shutting down. Defaults to True.
        """
        if self.scheduler.running:
            self.scheduler.shutdown(wait=wait)
            logger.info("Task scheduler shut down")

    def add_job(self, func: Callable, trigger: Any, job_id: str, name: str, **kwargs) -> None:
        """Adds a job to the scheduler.

        Args:
            func (Callable): The function to be scheduled.
            trigger (Any): The trigger for the job (e.g., 'date', 'interval', 'cron').
            job_id (str): A unique identifier for the job.
            name (str): A descriptive name for the job.
            **kwargs: Additional keyword arguments to pass to the job store.
        """

    def add_once_job(
            self,
            func: Callable,
            run_time: datetime,
            job_id: str | None = None,
            name: str | None = None,
            **kwargs,
    ) -> str:
        """Schedules a job to run once at a specific date and time.

        Args:
            func (Callable): The function to be scheduled.
            run_time (datetime): The exact date and time to run the job.
            job_id (str | None): Optional unique identifier for the job. If None, a UUID will be generated.
            name (str | None): Optional human-readable name for the job. Defaults to a generated name.
            **kwargs: Additional keyword arguments to pass to the job store.

        Returns:
            str: The unique ID of the added job.
        """
        job = self.scheduler.add_job(
            func,
            trigger=DateTrigger(run_date=run_time),
            id=job_id,
            name=name or f"once_{run_time.isoformat()}",
            kwargs=kwargs,
        )

        self._jobs[job.id] = job.name
        logger.info(f"Added one-time task: {job.name} (ID: {job.id}) at {run_time}")
        return job.id

    def add_daily_job(
            self,
            func: Callable,
            hour: int,
            minute: int = 0,
            job_id: str | None = None,
            name: str | None = None,
            **kwargs,
    ) -> str:
        """Schedules a job to run daily at a specific time.

        Args:
            func (Callable): The function to be scheduled.
            hour (int): The hour (0-23) at which the job should be executed.
            minute (int): The minute (0-59) at which the job should be executed. Defaults to 0.
            job_id (str | None): Optional unique identifier for the job. If None, a UUID will be generated.
            name (str | None): Optional human-readable name for the job. Defaults to a generated name.
            **kwargs: Additional keyword arguments to pass to the job store.

        Returns:
            str: The unique ID of the added job.
        """
        job = self.scheduler.add_job(
            func,
            trigger=CronTrigger(hour=hour, minute=minute),
            id=job_id,
            name=name or f"daily_{hour:02d}:{minute:02d}",
            kwargs=kwargs,
        )

        self._jobs[job.id] = job.name
        logger.info(f"Added daily task: {job.name} (ID: {job.id}) at {hour:02d}:{minute:02d}")
        return job.id

    def add_weekly_job(
            self,
            func: Callable,
            day_of_week: int,
            hour: int,
            minute: int = 0,
            job_id: str | None = None,
            name: str | None = None,
            **kwargs,
    ) -> str:
        """Schedules a job to run weekly on a specific day and time.

        Args:
            func (Callable): The function to be scheduled.
            day_of_week (int): The day of the week (0=Monday, 6=Sunday) for job execution.
            hour (int): The hour (0-23) at which the job should be executed.
            minute (int): The minute (0-59) at which the job should be executed. Defaults to 0.
            job_id (str | None): Optional unique identifier for the job. If None, a UUID will be generated.
            name (str | None): Optional human-readable name for the job. Defaults to a generated name.
            **kwargs: Additional keyword arguments to pass to the job store.

        Returns:
            str: The unique ID of the added job.
        """
        job = self.scheduler.add_job(
            func,
            trigger=CronTrigger(day_of_week=day_of_week, hour=hour, minute=minute),
            id=job_id,
            name=name or f"weekly_{day_of_week}_{hour:02d}:{minute:02d}",
            kwargs=kwargs,
        )

        self._jobs[job.id] = job.name
        logger.info(
            f"Added weekly task: {job.name} (ID: {job.id}) "
            f"on day {day_of_week} at {hour:02d}:{minute:02d}"
        )
        return job.id

    def add_interval_job(
            self,
            func: Callable,
            seconds: int = 0,
            minutes: int = 0,
            hours: int = 0,
            job_id: str | None = None,
            name: str | None = None,
            **kwargs,
    ) -> str:
        """Schedules a job to run repeatedly at a specified interval.

        Args:
            func (Callable): The function to be scheduled.
            seconds (int): The number of seconds for the interval. Defaults to 0.
            minutes (int): The number of minutes for the interval. Defaults to 0.
            hours (int): The number of hours for the interval. Defaults to 0.
            job_id (str | None): Optional unique identifier for the job. If None, a UUID will be generated.
            name (str | None): Optional human-readable name for the job. Defaults to a generated name.
            **kwargs: Additional keyword arguments to pass to the job store.

        Returns:
            str: The unique ID of the added job.
        """
        job = self.scheduler.add_job(
            func,
            trigger=IntervalTrigger(seconds=seconds, minutes=minutes, hours=hours),
            id=job_id,
            name=name or f"interval_{hours}h{minutes}m{seconds}s",
            kwargs=kwargs,
        )

        self._jobs[job.id] = job.name
        logger.info(
            f"Added interval task: {job.name} (ID: {job.id}) " f"Interval: {hours}h {minutes}m {seconds}s"
        )
        return job.id

    def add_cron_job(
            self,
            func: Callable,
            cron_expression: str,
            job_id: str | None = None,
            name: str | None = None,
            **kwargs,
    ) -> str:
        """Schedules a job to run based on a Cron expression.

        Args:
            func (Callable): The function to be scheduled.
            cron_expression (str): The Cron expression (e.g., '0 0 * * *' for daily at midnight) defining the schedule.
            job_id (str | None): Optional unique identifier for the job. If None, a UUID will be generated.
            name (str | None): Optional human-readable name for the job. Defaults to a generated name.
            **kwargs: Additional keyword arguments to pass to the job store.

        Returns:
            str: The unique ID of the added job.
        """
        job = self.scheduler.add_job(
            func,
            trigger=CronTrigger.from_crontab(cron_expression),
            id=job_id,
            name=name or f"cron_{cron_expression}",
            kwargs=kwargs,
        )

        self._jobs[job.id] = job.name
        logger.info(f"Added cron task: {job.name} (ID: {job.id}) Cron: {cron_expression}")
        return job.id

    def remove_job(self, job_id: str) -> bool:
        """Removes a scheduled job from the scheduler.

        Args:
            job_id (str): The unique ID of the job to remove.

        Returns:
            bool: True if the job was successfully removed, False otherwise.
        """
        try:
            self.scheduler.remove_job(job_id)
            job_name = self._jobs.pop(job_id, job_id)
            logger.info(f"Removed task: {job_id}")
            return True
        except Exception as e:
            logger.error(f"Failed to remove task: {job_id} - {e}")
            return False

    def pause_job(self, job_id: str) -> bool:
        """Pauses a scheduled job, preventing it from running until resumed.

        Args:
            job_id (str): The unique ID of the job to pause.

        Returns:
            bool: True if the job was successfully paused, False otherwise.
        """
        try:
            self.scheduler.pause_job(job_id)
            logger.info(f"Task paused: {job_id}")
            return True
        except Exception as e:
            logger.error(f"Failed to pause task: {job_id} - {e}")
            return False

    def resume_job(self, job_id: str) -> bool:
        """Resumes a paused scheduled job, allowing it to run again.

        Args:
            job_id (str): The unique ID of the job to resume.

        Returns:
            bool: True if the job was successfully resumed, False otherwise.
        """
        try:
            self.scheduler.resume_job(job_id)
            logger.info(f"Task resumed: {job_id}")
            return True
        except Exception as e:
            logger.error(f"Failed to resume task: {job_id} - {e}")
            return False

    def get_jobs(self) -> list[dict]:
        """Retrieves a list of all scheduled jobs currently managed by the scheduler.

        Returns:
            list[dict]: A list of dictionaries, where each dictionary represents a job
                and contains its 'id', 'name', 'next_run_time', and 'trigger'.
        """
        jobs = []
        for job in self.scheduler.get_jobs():
            jobs.append(
                {
                    "id": job.id,
                    "name": job.name,
                    "next_run_time": job.next_run_time,
                    "trigger": str(job.trigger),
                }
            )
        return jobs


class DownloadScheduler:
    """Manages download scheduling, including night and idle downloads.

    This class provides functionality to schedule downloads based on specific
    time windows (e.g., night downloads) or system idle conditions.
    It integrates with a `TaskScheduler` instance to manage the underlying jobs.
    """

    def __init__(self, task_scheduler: TaskScheduler):
        """Initializes the DownloadScheduler.

        Args:
            task_scheduler (TaskScheduler): An instance of the TaskScheduler to manage
                scheduled tasks for downloads.
        """
        self.scheduler = task_scheduler
        self._download_callback: Callable | None = None

    def set_download_callback(self, callback: Callable) -> None:
        """Sets the callback function to be executed when a scheduled download starts.

        This callback will be invoked by the scheduler when a download event is triggered.
        It is typically an asynchronous function that handles the actual download logic.

        Args:
            callback (Callable): The callback function to set. It should ideally be an async function.
        """
        self._download_callback = callback

    async def schedule_night_download(
            self, start_hour: int = 23, end_hour: int = 7
    ) -> tuple[str, str]:
        """Schedules night downloads to occur within a specified time window.

        This method sets up two daily jobs: one to start downloads at `start_hour`
        and another to pause them at `end_hour`. The actual download logic is handled
        by the `_download_callback` function.

        Args:
            start_hour (int): The hour (0-23) to start the download. Defaults to 23 (11 PM).
            end_hour (int): The hour (0-23) to end the download. Defaults to 7 (7 AM).

        Returns:
            tuple[str, str]: A tuple containing the job ID for the start download task
                and the job ID for the end download (pause) task.

        Raises:
            RuntimeError: If the download callback function has not been set using
                `set_download_callback`.
        """
        if self._download_callback is None:
            raise RuntimeError("Download callback function not set")

        # Start download task
        start_job_id = self.scheduler.add_daily_job(
            self._download_callback,
            hour=start_hour,
            minute=0,
            name=f"NightDownloadStart_{start_hour}:00",
        )

        # Pause download task
        async def pause_downloads():
            logger.info("Night download period ended, pausing downloads")
            # This should call the TaskManager's pause method
            pass

        end_job_id = self.scheduler.add_daily_job(
            pause_downloads,
            hour=end_hour,
            minute=0,
            name=f"NightDownloadEnd_{end_hour}:00",
        )

        logger.info(f"Night download plan set: {start_hour}:00 - {end_hour}:00")
        return start_job_id, end_job_id

    async def schedule_idle_download(self, check_interval_minutes: int = 5) -> str:
        """Schedules downloads to occur when the system is idle.

        This method sets up an interval job that periodically checks the system's
        CPU usage. If the CPU usage falls below a certain threshold (e.g., 20%),
        the download callback function is invoked to start downloads.

        Args:
            check_interval_minutes (int): The interval in minutes at which to check
                for idle status. Defaults to 5 minutes.

        Returns:
            str: The unique ID of the scheduled idle download job.

        Raises:
            RuntimeError: If the download callback function has not been set using
                `set_download_callback`.
        """

        async def check_and_download():
            import psutil

            cpu_percent = psutil.cpu_percent(interval=1)
            if cpu_percent < 20:  # CPU usage below 20%
                logger.info("System idle, starting download")
            if self._download_callback:
                await self._download_callback()

    job_id = self.scheduler.add_interval_job(
        check_and_download,
        minutes=check_interval_minutes,
        name="IdleDownloadCheck",
    )

    logger.info(f"Idle download plan set, check interval: {check_interval_minutes} minutes")
    return job_id


# Global scheduler instance
_global_scheduler: TaskScheduler | None = None


def get_scheduler() -> TaskScheduler:
    """Retrieves the global TaskScheduler instance.

    This function ensures that only one instance of `TaskScheduler` is created
    and used throughout the application. If an instance does not already exist,
    a new one is created and returned.

    Returns:
        TaskScheduler: The global TaskScheduler instance.
    """
    global _global_scheduler
    if _global_scheduler is None:
        _global_scheduler = TaskScheduler()
    return _global_scheduler
